
import java.io.*;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.util.ArrayList;
import java.lang.NumberFormatException;
import java.lang.SecurityException;
import java.rmi.RemoteException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.net.MalformedURLException;

class cliente{

  /**
   * In case this file is executed through the command line, the main procedure
   * exists to initialize a Cliente correctly. After data is read from standard
   * input, a new Cliente is instantiated with parameters specified by user
   * 
   * cliente -p <port> -n <node> [-d <download path>]
   * 
   * @param args Options specified by user
   */
  public static void main(String args[]){

    try{
      System.out.println("Iniciando el cliente, por favor espere...");
      BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
      cliente c = createCliente(br , System.out, args);

      //DEBUGGING
      System.out.println(c.toString());
      System.out.println("Cliente iniciado con éxito");
      
      while (c.prompt());
    }catch (ClienteException e){
      System.err.println("Error de ejecución: ");
      System.err.println(e.getMessage());
    }
  }

  /**
   * Given the array of options, validate and load options
   * 
   * @param args Array of options given in command line
   */
  public static cliente createCliente(BufferedReader in, PrintStream out, String args[])
    throws ClienteException
  {

    String options[] = {null, null, "."};
    NodoInterface node;
    int port;

    for (int i = 0; i < args.length; ++ i){
      if ("-p".equals(args[i])){
        options[0] = args[++ i];
      }else if ("-n".equals(args[i])){
        options[1] = args[++ i];
      }else if ("-d".equals(args[i])){
        options[2] = args[++ i];
      }else{
        throw new ClienteException("Uso: cliente -p <puerto> -n <nodo> [-d <directorio de descargas>]");
      }
    }

    //Options are kept in an array to ensure each one has been specified
    if (null == options[0] || null == options[1] || null == options[2]){
      throw new ClienteException("Uso: cliente -p <puerto> -n <nodo> [-d <directorio de descargas>]");
    }

    try{
      port = Integer.parseInt(options[0]);
    }catch(NumberFormatException e){
      throw new ClienteException("Puerto '" + options[0] + "' inválido: " + e.getMessage());
    }

    try{
        node = (NodoInterface) Naming.lookup("rmi://" + options[1] + ":" + options[0] + "/NodeService");
    }catch(RemoteException re){
        throw new ClienteException("Excepcion del host remoto: " + re.getMessage());
    }catch(NotBoundException nbe){
        throw new ClienteException("Servicio solicitado no válido: " + nbe.getMessage());
    }catch(MalformedURLException mue){
        throw new ClienteException("URL mal formada: " + mue.getMessage());
    }

    String downloadDir = options[2];
    if (downloadDir.length() - 1 == downloadDir.lastIndexOf('/')){
      downloadDir = downloadDir.substring(0, downloadDir.length() - 1);
    }

    return new cliente(in, out, port, node, downloadDir, options[1]);
  }

  private static String pad(String s, int l){
    while (s.length() < l){
      s += " ";
    }
    return s;
  }

  //PrintStream to print output
  private PrintStream out;

  //BufferedReader to read from
  private BufferedReader in;

  //Port to use to make requests to node
  private int port;

  //Node to make requests to
  private NodoInterface node;

  //Nodes hostName
  private String hostName;

  //Download directory
  private String downloadDir;

  //Last consult
  private ArrayList<Track> lastConsult = null;

  //Help information about commands interpreted by this program
  private String help =
    "Las opciones existentes son:\n" +
    "C [-a <artista> | -t <titulo> | -d <disco> | -g <genero> | -y <año> | -l <letra> | -c <compositor>]  \n" +
    "                                Consultar canciones\n" +
    "                                -a busca por artista con la cadena especificada\n" +
    "                                -t busca por título con la cadena especificada\n" +
    "                                -d busca por disco con la cadena especificada\n" +
    "                                -g busca por genero con la cadena especificada\n" +
    "                                -y busca por año con la cadena especificada\n" +
    "                                -l busca por letra con la cadena especificada\n" +
    "                                -c busca por compositor con la cadena especificada\n" +
    "D num                           Descarga la canción especificada por el identificador\n" +
    "A                               Muestra los nodos alcanzables desde el cliente\n" +
    "Q                               Salir";

  /**
   * Constructor
   * 
   * This class acts as node client. Given input of a user by in Scanner, it can perform
   * requests to the node Host. It can download files to the downloadDir. To interact
   * with user it uses the out PrintStream
   * 
   * @param in           File descriptor reader to interact with user
   * @param out          File descriptor writer to interact with user
   * @param port         Port on node
   * @param node         Node where this client looks for when querying for files
   * @param downloadDir  Download directory for files requested by user
   * @param hn           Host name of the node that is being contacted
   */
  public cliente(BufferedReader in, PrintStream out, int port, NodoInterface node, String downloadDir, String hn)
    throws ClienteException
  {
    if (port < 1024 || 65535 < port){
      throw new ClienteException("Número de puerto " + port + " inválido");
    }
    if (!(new File(downloadDir)).exists()){
      throw new ClienteException("La carpeta de descargas '" + downloadDir + "' no existe");
    }

    this.in = in;
    this.out = out;
    this.port = port;
    this.node = node;
    this.downloadDir = downloadDir;
    this.hostName = hn;
  }

  /**
   * Prompts the user trough the local scanner to make a request
   */
  public boolean prompt()
    throws ClienteException
  {
    //Read command line
    String request;
    try{
      request = in.readLine();
    }catch(IOException e){
      throw new ClienteException("Error leyendo línea de comando del input: " + e.getMessage());
    }
    if (null == request){
      out.println(help);
      return true;
    }

    //Split command by spaces
    String[] opts = request.split("\\s+");
    if (0 == opts.length){
      out.println(help);
      return true;
    }

    String res;
    //Command execution 
    if ("c".equals(opts[0].toLowerCase())){
      if (3 <= opts.length){
        for(int i = 3; i < opts.length; ++i){
            opts[2] += " " + opts[i];
        }
        if ("-t".equals(opts[1])){
          //Consult songs by title
          res = consult("", opts[2], "", "", "", "", "");
        }else if ("-a".equals(opts[1])){
          //Consult songs by creator
          res = consult(opts[2], "", "", "", "", "", "");
        }else if ("-d".equals(opts[1])){
          res = consult("", "", opts[2], "", "", "", "");
        }else if ("-y".equals(opts[1])){
          res = consult("", "", "", opts[2], "", "", "");
        }else if ("-g".equals(opts[1])){
          res = consult("", "", "", "", opts[2], "", "");
        }else if ("-l".equals(opts[1])){
          res = consult("", "", "", "", "", opts[2], "");
        }else if ("-c".equals(opts[1])){
          res = consult("", "", "", "", "", "", opts[2]);
        }else if ("-h".equals(opts[1])){
          res = help;
        }else{
          res = help;
        }
      }else if (1 == opts.length){
        //Consult all songs
        res = consult("", "", "", "", "", "", "");
      }else{
        res = help;
      }
    }else if ("a".equals(opts[0].toLowerCase())){
      //Show reachable nodes
      res = reachableNodes();
    }else if ("d".equals(opts[0].toLowerCase())){
      if (2 == opts.length){
        //Download specified track
        try{
          res = download(Integer.parseInt(opts[1]) - 1);
        }catch(NumberFormatException e){
          res = help;
        }
      }else{
        res = help;
      }
    }else if ("q".equals(opts[0].toLowerCase())){
      //Exit
      return false;
    }else{
      //Invalid command
      res = help;
    }
    //In case command returns content which wishes to show
    if (null != res){
      out.println(res);
    }
    return true;
  }

  /**
   * Consults songs on reachable nodes by creator/title whenever they are specified
   * 
   * @param creator Substring which should match with track's creator
   * @param title   Substring which should match with track's title
   */
  private String consult(String creator, String title, String album, String year, String genre, String lyric, String composer)
    throws ClienteException
  {
    ArrayList<Track> response;
    Track consult;

    try{
        consult = new Track("", title.toLowerCase(), creator.toLowerCase(), 
                            album.toLowerCase(), year.toLowerCase(),
                            genre.toLowerCase(), lyric.toLowerCase(),
                            composer.toLowerCase(), "");
      response = this.node.consult(new Request(consult, new ArrayList<String>(), this.hostName));
    }catch(HostException e){
      throw new ClienteException("Error enviando solicitud de consulta: " + e.getMessage());
    }catch(RemoteException re){
        throw new ClienteException("Error en comunicación con el host: " + re.getMessage());
    }catch(TrackException te){
        throw new ClienteException("Error al crear el track de consulta: " + te.getMessage());
    }

    this.lastConsult = response;

    //Calculate consult table width
    int mNum = 0, mCreator = "Autor".length(), mTitle = "Canción".length(), mNodeName = "Nodo".length();
    for (Track t : this.lastConsult){
      ++ mNum;
      mCreator = Math.max(mCreator, t.getCreator().length());
      mTitle = Math.max(mTitle, t.getTitle().length());
      mNodeName = Math.max(mNodeName, t.getNodeName().length());
    }
    mNum = Math.max(4, Integer.toString(mNum).length() + 1);
    ++ mCreator;
    ++ mTitle;
    ++ mNodeName;

    //Create table
    String output = pad("Num", mNum) +
                    pad("Autor", mCreator) +
                    pad("Canción", mTitle) +
                    pad("Nodo", mNodeName) + "\n";
    int i = 0;
    for (Track t : this.lastConsult){
      output += pad(Integer.toString(++ i), mNum) +
                pad(t.getCreator(), mCreator) +
                pad(t.getTitle(), mTitle) + 
                pad(t.getNodeName(), mNodeName) + "\n";
    }
    output = output.substring(0, output.length() - 1);
    return output;
  }

  /**
   * Shows all nodes which are reachable from here
   */
  private String reachableNodes()
    throws ClienteException
  {

    ArrayList<String> response;
    try{
      response = this.node.getReachables(new ArrayList<String>());
    }catch(HostException e){
      throw new ClienteException("Error enviando solicitud de nodos alcanzables: " + e.getMessage());
    }catch(RemoteException re){
      throw new ClienteException("Error en la comunicación con el host: " + re.getMessage());
    }

    ArrayList<String> nodes = response;

    //Create table with nodes
    String output = "";
    for (String n : nodes){
      output += n + '\n';
    }
    output = output.substring(0, output.length() - 1);

    return output;
  }

  /**
   * Downloads the specified track
   * 
   * @param num Index of track in last consult
   */
  private String download(int num)
    throws ClienteException
  {
    Track t;
    NodoInterface nodeToDownload;
    File response;
    BufferedOutputStream out;
    BufferedInputStream is;
    // 1MB buffer to read from the socket. This ensures that the bytes read won't
    // exceed the permitted capcity
    byte buffer[] = new byte[1048576];
    int read;
    long size;

    if (lastConsult.size() <= num){
      return "El índice " + (num + 1) + " no es válido";
    }

    t = lastConsult.get(num);
    try{
        nodeToDownload = (NodoInterface) Naming.lookup("rmi://" + t.getServerName() + ":" + this.port + "/NodeService");
    }catch(RemoteException re){
        throw new ClienteException("Excepcion del host remoto: " + re.getMessage());
    }catch(NotBoundException nbe){
        throw new ClienteException("Servicio solicitado no válido: " + nbe.getMessage());
    }catch(MalformedURLException mue){
        throw new ClienteException("URL mal formada: " + mue.getMessage());
    }

    try{
      response = nodeToDownload.download(new Request(t.getLocation()));
    }catch(RemoteException e){
      throw new ClienteException("Error enviando solicitud de descarga: " + e.getMessage());
    }
    
    if (0 <= (size = response.length())){
        try{
            System.out.println("Descargando el archivo...");
            out = new BufferedOutputStream(new FileOutputStream(downloadDir + "/" + t.getTitle() + ".mp3"));
            is = new BufferedInputStream(new FileInputStream(response));
            while(size > 0){
                read = is.read(buffer, 0, buffer.length);
                out.write(buffer, 0, read);
                size -= read;
            }
            System.out.println("Archivo descargado con éxito");
        }catch(IOException io){
            System.out.println("Error creando el archivo para descargar");
        }
    }

    return null;
  }

  /**
   * String representation of this object for debugging purposes
   */
  public String toString(){
    return "##################### PORT #####################\n" +
           port + '\n' +
           "##################### NODE #####################\n" +
           node.toString() + '\n' +
           "##################### DOWNLOAD DIR #####################\n" +
           downloadDir;
  }
}